﻿/* **********************************************************************************
 * Copyright (c) 2011 John Hughes
 *
 * Elve Touch is licenced under the Microsoft Reciprocal License (Ms-RL).
 *
 * Project Website: http://elvetouch.codeplex.com/
 * **********************************************************************************/

using System;
//using System.Linq;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.Threading;
using Prophecy.Communication;

namespace ElveTouch
{
    public partial class TouchScreenInterface : Form
    {
        [System.Runtime.InteropServices.DllImport("coredll")]
        private static extern bool ShowWindow(IntPtr hwnd, int nCmdShow);
        private const int SW_MINIMIZE = 6;

        private static TouchTcpClient _tcpConnection;

        private Bitmap _drawingBitmap;
        private Graphics _graphics;

        private string _username, _password, _server;
        private int _port;
        private RenderingMode _renderingMode;
        
        private bool _hideCursor;
        bool _fullscreenOnStartup;
        bool _isFullscreen;
        bool _kiosk;
        bool _fitToScreenOrWindow;

        Size ActualTouchScreenSize;

        bool _showingUnconnectedMenu; // used to keep track of when the menu is show when not connected.  This is because we show the menu when there is a mouse click, but if you click out of the menu it wants to show again.

        bool _forceShowLoginForm;
        string _loginFormMessage;

        public TouchScreenInterface()
        {
            // This prevents flicker by enabling double buffering. Basically this prevents the system from drawing the form's background color which we don't need before I draw the bitmap in the forms OnPaint() method.
            // For good details on SetStyle and flicker see: http://alt.pluralsight.com/wiki/default.aspx/Craig/FlickerFreeControlDrawing.html
            this.SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true);

            InitializeComponent();

            // Disable auto-scaling so that DrawText draws the text at the originally specified size and is not drawn larger if the user's PocketPC has a higher resolution screen.
            // This is already set in the form designer, but I just want it to be explicit so I don't forget it needs to be set.
            this.AutoScaleMode = AutoScaleMode.None;
        }



        private void TSForm_Load(object sender, EventArgs e)
        {
            //***************************************************************
            // Get configuration from file.
            //***************************************************************
            try
            {
                // FileFormat:
                //   username\r\n
                //   password\r\n
                //   server\r\n
                //   port\r\n
                //   renderingmode\r\n
                string filePath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), @"Codecore Technologies\Elve\TouchScreenViewerLite.config");
                if (System.IO.File.Exists(filePath))
                {
                    byte[] encrypted = System.IO.File.ReadAllBytes(filePath);
                    byte[] decrypted = Crypto.DecryptAES(encrypted, "fgYFKYvhksafgYG");
                    string s = System.Text.UTF8Encoding.UTF8.GetString(decrypted);
                    string[] lines = s.Split(new string[] { "\r\n" }, StringSplitOptions.None);
                    _username = lines[0];
                    _password = lines[1];
                    _server = lines[2];
                    _port = int.Parse(lines[3]);
                    _renderingMode = (RenderingMode)int.Parse(lines[4]);
                }
            }
            catch
            {
                // ignore failure to read configuration, no big deal.
            }

            //***************************************************************
            // Read configuration from command line
            //***************************************************************
            CommandLineArgs args = new CommandLineArgs();

            _hideCursor = args.HideMouse;
            _fullscreenOnStartup = !args.Window;
            _kiosk = args.Kiosk;
            _fitToScreenOrWindow = args.FitToScreen;

            string[] commandLineArgsArray = Environment.GetCommandLineArgs();
            if (commandLineArgsArray.Length > 1)
            {
                // Remove exe path from command line args.
                string commandLine = string.Join(" ", commandLineArgsArray, 1, commandLineArgsArray.Length - 1).ToLower();
                if (commandLine.Contains("/username"))
                    _username = args.Username;
                if (commandLine.Contains("/password"))
                    _password = args.Password;
                if (commandLine.Contains("/server"))
                    _server = args.Server;
                if (commandLine.Contains("/port"))
                    _port = args.Port;
                if (commandLine.Contains("/renderingmode"))
                    _renderingMode = args.RenderingMode;
            }

            //***************************************************************
            // Validate configuration
            //***************************************************************
            if (_username == null)
                _username = "";
            if (_password == null)
                _password = "";
            if (_server == null)
                _server = "";
            if (_port <= 0 || _port > 65535)
                _port = 33907;



            if (_hideCursor)
                Cursor.Hide();

            if (_fullscreenOnStartup)
                fullscreen();

            if (_kiosk)
            {
                cmsViewerMenu.Enabled = false;

                _hookID = SetKeyboardHook(_proc);
            }

            // Create the bitmap to draw to.
            createDrawingBitmap(this.ClientSize);

            this.Show();

            loginFormAndConnect(args.AutoSignIn == false, "");
        }

        private void createDrawingBitmap(Size bitmapSize)
        {
            // Remember the old object to prevent potential async errors and since we use it to copy the current image to the new bitmap.
            Bitmap prevBitmap = _drawingBitmap;
            Graphics prevGraphics = _graphics;

            _drawingBitmap = new Bitmap(bitmapSize.Width, bitmapSize.Height, System.Drawing.Imaging.PixelFormat.Format32bppRgb);
            _graphics = Graphics.FromImage(_drawingBitmap);
            _graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;

            // Copy the previous bitmap
            if (prevGraphics != null && prevBitmap != null)
            {
                _graphics.DrawImage(_drawingBitmap, 0, 0);
            }

            // Dispose of the old bitmap
            if (prevGraphics != null)
                prevGraphics.Dispose();
            if (prevBitmap != null)
                prevBitmap.Dispose();
        }

        void loginFormAndConnect(bool forceShowLoginForm, string message)
        {
            if (forceShowLoginForm || string.IsNullOrEmpty(_username) || string.IsNullOrEmpty(_server))
            {
                using (SignIn form = new SignIn(message, _username, _password, _server, _port, _renderingMode))
                {
                    if (form.ShowDialog(this) == DialogResult.Cancel)
                        return;

                    _username = form.UserName;
                    _password = form.Password;
                    _server = form.Hostname;
                    _port = form.Port;
                    _renderingMode = (RenderingMode)form.Quality;

                    // Handle configuration file.
                    string folderPath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), @"Codecore Technologies\Elve");
                    string filePath = System.IO.Path.Combine(folderPath, "TouchScreenViewerLite.config");
                    if (form.RememberSettings)
                    {
                        try
                        {
                            // Save encrypted configuration.
                            // FileFormat:
                            //   username\r\n
                            //   password\r\n
                            //   server\r\n
                            //   port\r\n
                            //   renderingmode\r\n
                            string s = _username + "\r\n" + _password + "\r\n" + _server + "\r\n" + _port + "\r\n" + ((int)_renderingMode) + "\r\n";
                            byte[] utf8 = System.Text.UTF8Encoding.UTF8.GetBytes(s);
                            byte[] encypted = Crypto.EncryptAES(utf8, "fgYFKYvhksafgYG");
                            System.IO.Directory.CreateDirectory(folderPath);
                            System.IO.File.WriteAllBytes(filePath, encypted);
                        }
                        catch
                        {
                            // ignore failure to save, it's not a major issue.
                        }
                    }
                    else
                    {
                        try
                        {
                            System.IO.File.Delete(filePath);
                        }
                        catch
                        {
                            // ignore failure to delete
                        }
                    }
                }
            }

            showStatusOnBlackBackground(TouchTcpClientState.AttemptingToConnect);
            Refresh(); // Force the form to refresh now, otherwise there will be a white background for a second or so.... I'm not sure how to prevent the white background from showing.

            _showingUnconnectedMenu = false;

            _mouseMoveStopWatch.Reset();
            _mouseMoveStopWatch.Start();

            // Set up persistent TCP connection to master server.
            // TODO: set actual pixel depth and alpha support
            //_tcpConnection = new TouchTcpClient(ClientConfig.Config.MasterServerHostName, ClientConfig.Config.Username, ClientConfig.Config.Password, this, ClientConfig.Config.RenderingMode, 24, false);
            _tcpConnection = new TouchTcpClient(_server, _port, _username, _password, this, _renderingMode, 24, false);
            _tcpConnection.TouchTcpClientStateChanged += new EventHandler<TouchTcpClientStateChangedEventArgs>(_tcpConnection_TouchTcpClientStateChanged);
            _tcpConnection.TouchTcpAuthenticationResultReceived += new EventHandler<TouchTcpAuthenticationResultReceivedEventArgs>(_tcpConnection_TouchTcpAuthenticationResultReceived);
            _tcpConnection.ConnectionEstablished += new EventHandler<EventArgs>(_tcpConnection_ConnectionEstablished);
            _tcpConnection.ConnectionLost += new EventHandler<EventArgs>(_tcpConnection_ConnectionLost);
            _tcpConnection.ConnectionAttemptFailed += new EventHandler<ConnectionAttemptFailedEventArgs>(_tcpConnection_ConnectionAttemptFailed);
            _tcpConnection.TouchTcpClientException += new EventHandler<TouchTcpClientExceptionEventArgs>(_tcpConnection_TouchTcpClientException);
            _tcpConnection.CommunicationStarted += new EventHandler(_tcpConnection_CommunicationStarted);
            _tcpConnection.CommunicationEnded += new EventHandler(_tcpConnection_CommunicationEnded);
            _tcpConnection.Open(); // In this case Open is asynchronous.
        }

        void _tcpConnection_CommunicationStarted(object sender, EventArgs e)
        {
            this.Invoke(new MethodInvoker( delegate
            {
                // TODO: show an activiity indicator on the touch screen interface.
                //this.Text = "Communicating";
                //progressBar1.Visible = true;
                //pictureBox1.Visible = true;
                //pictureBox1.Image = ElveTouch.Properties.Resources.activityindicator;
                Cursor = Cursors.WaitCursor;
            }));
        }

        void _tcpConnection_CommunicationEnded(object sender, EventArgs e)
        {
            this.Invoke(new MethodInvoker(delegate
            {
                // TODO: show an activiity indicator on the touch screen interface.
                //this.Text = "Idle";
                //progressBar1.Visible = false;
                //pictureBox1.Visible = false;
                //pictureBox1.Image = null;
                Cursor = Cursors.Default;
            }));
        }




        public Graphics Graphics
        {
            get { return _graphics; }
        }

        public List<string> EventActionExceptions = new List<string>();
        public void AddTouchScreenEvent(Exception ex)
        {
            EventActionExceptions.Add(DateTime.Now.ToString() + "  " + ex.Message);

            if (EventActionExceptions.Count > 100)
                EventActionExceptions.RemoveAt(0);
        }

        void _tcpConnection_ConnectionEstablished(object sender, EventArgs e)
        {
            AddTouchScreenEvent(new Exception("A persistent connection was established with the server."));

            InvalidateFormOnFormThread(); // redraw so that the status is removed.
        }

        void _tcpConnection_ConnectionLost(object sender, EventArgs e)
        {
            AddTouchScreenEvent(new Exception("The persistent connection with the server was lost."));

            InvalidateFormOnFormThread(); // redraw so that the status is removed.
        }

        void _tcpConnection_ConnectionAttemptFailed(object sender, ConnectionAttemptFailedEventArgs e)
        {
            //Reconnect(true, "Failed to connect. " + e.Exception.Message);

            showMessageOnBlackBackground("Failed to connect to '" + _server + "'.\r\n" + e.Exception.Message + "\r\nAn auto-reconnect will occur in a few seconds.");

            Reconnect(false, e.Exception.ToString(), 10000); // delay 10 seconds so the user can read the error.
        }

        void _tcpConnection_TouchTcpClientStateChanged(object sender, TouchTcpClientStateChangedEventArgs e)
        {
            showStatusOnBlackBackground(e.State);
        }

        void _tcpConnection_TouchTcpClientException(object sender, TouchTcpClientExceptionEventArgs e)
        {
            showMessageOnBlackBackground("An error occurred.\r\n" + e.Message + "\r\nAn auto-reconnect will occur in a few seconds.");

            Reconnect(false, e.Message, 10000); // delay 10 seconds so the user can read the error.
        }

        private void showStatusOnBlackBackground(TouchTcpClientState state)
        {
            string text = "";
            switch (state)
            {
                case TouchTcpClientState.AttemptingToConnect:
                    text = "Connecting...";
                    break;
                case TouchTcpClientState.Authenticating:
                    text = "Logging On...";
                    break;
                case TouchTcpClientState.Authenticated:
                    text = "Downloading initial screen...";
                    break;
                case TouchTcpClientState.FailedAuthentication:
                    text = "Unsuccessful Authentication.";
                    break;
            }

            showMessageOnBlackBackground(text);
        }

        private void showMessageOnBlackBackground(string text)
        {
            // Use a black background.
            using (Brush brush = new SolidBrush(Color.Black))
                _graphics.FillRectangle(brush, 0, 0, _drawingBitmap.Width, _drawingBitmap.Height);

            // Draw state text
            using (Font font = new Font("Tahoma", 15, FontStyle.Bold))
            {
                using (StringFormat sf = new StringFormat())
                {
                    sf.Alignment = StringAlignment.Center;
                    sf.LineAlignment = StringAlignment.Center;
                    //sf.FormatFlags = ;
                    //sf.Trimming = trimming;

                    using (Brush brush = new SolidBrush(Color.White))
                    {
                        // In case the window has changed size, get the size.
                        int width = Math.Min(_drawingBitmap.Width, this.ClientRectangle.Width);
                        int height = Math.Min(_drawingBitmap.Height, this.ClientRectangle.Height);
                        _graphics.DrawString(text, font, brush, new Rectangle(0, 0, width, height), sf);
                    }
                }
            }

            // Show the new background.
            InvalidateFormOnFormThread();
        }

        void _tcpConnection_TouchTcpAuthenticationResultReceived(object sender, TouchTcpAuthenticationResultReceivedEventArgs e)
        {
            AddTouchScreenEvent(new Exception("The persistent connection received an authentication result of '" + e.Result.ToString() + "'."));

            if (e.Result != TouchServiceTcpCommunicationAuthenticationResults.Success)
            {
                string message = "";
                if (e.Result == TouchServiceTcpCommunicationAuthenticationResults.Invalid)
                    message = "The username or password is incorrect.";
                else if (e.Result == TouchServiceTcpCommunicationAuthenticationResults.Disabled)
                    message = "The user account has been disabled.";
                else if (e.Result == TouchServiceTcpCommunicationAuthenticationResults.BlockedByTimeRestrictions)
                    message = "The user account is currently blocked by time restrictions, please try again later.";

                Reconnect(true, message);

                return;
            }

            ActualTouchScreenSize = e.TouchScreenSize;
            createDrawingBitmap(e.TouchScreenSize);

            if (_isFullscreen == false)
                setFormSize(ActualTouchScreenSize);
        }

        delegate void setFormSizeDelegate(Size size);
        private void setFormSize(Size tsSize)
        {
            if (this.InvokeRequired)
            {
                setFormSizeDelegate func = new setFormSizeDelegate(setFormSize);
                this.Invoke(func, new object[] { tsSize });
                return;
            }

            this.ClientSize = tsSize;
        }


        private void tsmiRefreshDisplay_Click(object sender, EventArgs e)
        {
            try
            {
                if (_tcpConnection != null && _tcpConnection.Connected)
                {
                    if (_tcpConnection.RenderingMode == RenderingMode.DrawCommands)
                        _tcpConnection.RequestRedraw(this.ClientRectangle);
                    else
                        _tcpConnection.RequestSnapshot(this.ClientRectangle);
                }
            }
            catch
            {
                // do nothing.
            }
        }

        private void tsmiViewerToggleMousePointer_Click(object sender, EventArgs e)
        {
            toggleMousePointer();
        }

        private void tsmiViewerToggleFullscreen_Click(object sender, EventArgs e)
        {
            ToggleFullscreen();
        }

        private void tsmiViewerShowErrorsWindow_Click(object sender, EventArgs e)
        {
            showErrorsWindow();
        }

        private void tsmiViewerAbout_Click(object sender, EventArgs e)
        {
            using (AboutBox form = new AboutBox())
                form.ShowDialog(this);// The form will continue to update just fine even though this is modal. (I have tested it.)
        }

        private void tsmiViewerExit_Click(object sender, EventArgs e)
        {
            if (MessageBox.Show(this, "Are you sure you want to exit the application?", "Touch Screen Viewer", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button1) == DialogResult.No)
                return;

            this.Close();
        }


        void toggleMousePointer()
        {
            _hideCursor = !_hideCursor;

            if (_hideCursor)
                Cursor.Hide();
            else
                Cursor.Show();
        }

        private void fullscreen()
        {
            _isFullscreen = true;
            this.FormBorderStyle = FormBorderStyle.None;
            //base.WindowState = FormWindowState.Maximized;

            // Fill the current screen
            Screen currentScreen = Screen.FromControl(this);
            this.Location = currentScreen.Bounds.Location;
            this.Size = currentScreen.Bounds.Size;

            this.TopMost = true;
        }

        private void windowed()
        {
            if (_kiosk)
                return;

            _isFullscreen = false;
            this.FormBorderStyle = FormBorderStyle.FixedDialog;
            //base.WindowState = FormWindowState.Normal;

            this.TopMost = false;

            Size size = ActualTouchScreenSize;
            if (size.IsEmpty) // it will be empty if it's not set yet
                size = new Size(800, 600);

            // Center the form in the current screen
            Screen currentScreen = Screen.FromControl(this);
            int boundWidth = currentScreen.Bounds.Width;
            int boundHeight = currentScreen.Bounds.Height;
            int x = boundWidth - size.Width;
            int y = boundHeight - size.Height;
            this.Location = new Point(currentScreen.Bounds.Left + (x / 2), currentScreen.Bounds.Top + (y / 2));

            this.ClientSize = size; // the form's ClientSize is incorrect when going to windowed mode from fullscreen on startup.
        }

        internal void ToggleFullscreen()
        {
            // The initial form size is sent at the time of the connection. If the user changes to/from windowed mode the app can deal with the change as it likes, such as reconnecting and sending the new size, or scaling or scrolling the window, etc.
            // NOTE: This function can also be called from a the TouchTcpClient.
            if (_kiosk == false)
            {
                this.Invoke(
                    new MethodInvoker(
                    delegate()
                    {
                        if (_isFullscreen)
                            windowed();
                        else
                            fullscreen();
                    }));
            }
        }

        internal void ToggleFitToScreen()
        {
            _fitToScreenOrWindow = !_fitToScreenOrWindow;
            InvalidateFormOnFormThread();
        }

        private void showErrorsWindow()
        {
            if (EventActionExceptions.Count == 0)
            {
                MessageBox.Show(this, "No errors have occurred.", "Touch Screen Viewer", MessageBoxButtons.OK, MessageBoxIcon.Information);
                return;
            }
            else
            {
                using (LogForm form = new LogForm(EventActionExceptions))
                    form.ShowDialog(this); // need to specify this window as parent in case we are in fullscreen mode and are a topmost window
            }
        }

        #region Intercept low level keys (used in kiosk mode)
        private static IntPtr SetKeyboardHook(LowLevelKeyboardProc proc)
        {
            using (Process curProcess = Process.GetCurrentProcess())
            {
                using (ProcessModule curModule = curProcess.MainModule)
                {
                    return SetWindowsHookEx(WH_KEYBOARD_LL, proc,
                        GetModuleHandle(curModule.ModuleName), 0);
                }
            }
        }

        private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);

        private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
        {
            if (nCode >= 0 && (wParam == (IntPtr)WM_KEYDOWN || wParam == (IntPtr)WM_SYSKEYDOWN))
            {
                int vkCode = Marshal.ReadInt32(lParam);
                //System.Diagnostics.Debug.WriteLine(nCode + "  0x" + wParam.ToString("X") + "  " + (Keys)vkCode);
                if ((Keys.Tab == (Keys)vkCode && Keys.Alt == Control.ModifierKeys) // ALT-TAB
                    || (Keys.Escape == (Keys)vkCode && Keys.Alt == Control.ModifierKeys) //ALT-ESCAPE
                    || (Keys.Escape == (Keys)vkCode && Keys.Control == Control.ModifierKeys) // CTRL-ESCAPE
                    || (Keys.Escape == (Keys)vkCode && (Keys.Control | Keys.Shift) == Control.ModifierKeys) // CTRL-SHIFT_ESCAPE
                    || Keys.LWin == (Keys)vkCode // LEFT WINDOWS (also LWINDOWS-TAB)
                    || Keys.RWin == (Keys)vkCode // RIGHT WINDOWS (also RWINDOWS-TAB)
                    || Keys.Apps == (Keys)vkCode) // APPS KEY
                {
                    return new IntPtr(1);
                }
            }
            return CallNextHookEx(_hookID, nCode, wParam, lParam);
        }


        private const int WH_KEYBOARD_LL = 13;
        private const int WM_KEYDOWN = 0x0100;
        private const int WM_KEYUP = 0x0101;
        private const int WM_CHAR = 0x0102;
        private const int WM_SYSKEYDOWN = 0x0104;
        private static LowLevelKeyboardProc _proc = HookCallback;
        private static IntPtr _hookID = IntPtr.Zero;
        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr SetWindowsHookEx(int idHook,
            LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool UnhookWindowsHookEx(IntPtr hhk);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode,
            IntPtr wParam, IntPtr lParam);

        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr GetModuleHandle(string lpModuleName);
        #endregion
        

        private void TouchScreenInterface_Closing(object sender, CancelEventArgs e)
        {
            if (_tcpConnection != null)
            {
                _tcpConnection.Dispose();
                _tcpConnection = null;
            }
        }


        private bool _mouseButtonIsDown = false;
        private void TouchScreenInterface_MouseDown(object sender, MouseEventArgs e)
        {
            if (_tcpConnection != null && _tcpConnection.Connected)
            {
                if (e.Button == MouseButtons.Left)
                {
                    _mouseButtonIsDown = true;
                    Point p = windowPointToBitmapPoint(e.Location);
                    _tcpConnection.TouchEvent(TouchEventType.TouchDown, p.X, p.Y);
                }
            }
        }

        private void TouchScreenInterface_Click(object sender, EventArgs e)
        {
            if (_tcpConnection != null && _tcpConnection.Connected)
            {
                Point p = windowPointToBitmapPoint(this.PointToClient(MousePosition));
                _tcpConnection.TouchEvent(TouchEventType.Touched, p.X, p.Y);
            }
            else
            {
                if (_showingUnconnectedMenu == false)
                {
                    cmsViewerMenu.Show(MousePosition);
                    _showingUnconnectedMenu = true;
                }
                else
                    _showingUnconnectedMenu = false;
            }
        }

        private void TouchScreenInterface_MouseUp(object sender, MouseEventArgs e)
        {
            if (_tcpConnection != null && _tcpConnection.Connected)
            {
                if (e.Button == MouseButtons.Left)
                {
                    _mouseButtonIsDown = false;
                    Point p = windowPointToBitmapPoint(e.Location);
                    _tcpConnection.TouchEvent(TouchEventType.TouchUp, p.X, p.Y);
                }
            }
        }

        private void TouchScreenInterface_DoubleClick(object sender, EventArgs e)
        {
            if (_tcpConnection != null && _tcpConnection.Connected)
            {
                Point p = windowPointToBitmapPoint(this.PointToClient(MousePosition));
                _tcpConnection.TouchEvent(TouchEventType.DoubleTouch, p.X, p.Y);
            }
        }

        private System.Diagnostics.Stopwatch _mouseMoveStopWatch = new System.Diagnostics.Stopwatch();
        private void TouchScreenInterface_MouseMove(object sender, MouseEventArgs e)
        {
            if (_mouseButtonIsDown)
            {
                // Throttle the sent mouse move commands so we don't overload the server.
                if (_mouseMoveStopWatch.ElapsedMilliseconds > 125)
                {
                    Point p = windowPointToBitmapPoint(e.Location);
                    _tcpConnection.TouchEvent(TouchEventType.TouchMove, p.X, p.Y);
                    _mouseMoveStopWatch.Reset();
                    _mouseMoveStopWatch.Start();
                }
            }
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            Graphics g = e.Graphics;

            try
            {
                if (_fitToScreenOrWindow)
                {
                    // Fit the image to the screen/window.
                    Rectangle rectSource;
                    Rectangle rectDest;
                    ImageSizeModeHelper.ImageRectangleFromSizeMode(_drawingBitmap, this.ClientRectangle, ImageSizeMode.Zoom, out rectSource, out rectDest);

                    // Fill the areas not covered by the bitmap (top/bottom or left/right).
                    if (rectDest.Location != Point.Empty)
                    {
                        using (Brush brush = new SolidBrush(Color.Black))
                        {
                            if (rectDest.Top != 0)
                                // fill top and bottom rectangles
                                g.FillRectangles(brush, new Rectangle[] { new Rectangle(0, 0, this.ClientRectangle.Width, rectDest.Top), new Rectangle(0, rectDest.Bottom, this.ClientRectangle.Width, this.ClientRectangle.Height - rectDest.Bottom + 1) });
                            else if (rectDest.Left != 0)
                                // fill left and right rectangles
                                g.FillRectangles(brush, new Rectangle[] { new Rectangle(0, 0, rectDest.Left, this.ClientRectangle.Height), new Rectangle(rectDest.Right - 1, 0, this.ClientRectangle.Width - rectDest.Right + 1, this.ClientRectangle.Height) });
                        }
                    }

                    // Draw the bitmap.
                    g.DrawImage(_drawingBitmap, rectDest, rectSource.X, rectSource.Y, rectSource.Width, rectSource.Height, GraphicsUnit.Pixel);
                }
                else
                {
                    Point p = getDrawingOffset();
                    g.DrawImage(_drawingBitmap, p.X, p.Y); //, _drawingBitmap.Width, _drawingBitmap.Height);
                }
            }
            catch
            {
                // Sometimes g.DrawImage throws an exception... but I'm not sure why yet.
                // UPDATE: I found a site on the internet which might explain this:
                // http://alt.pluralsight.com/wiki/default.aspx/Craig/FlickerFreeControlDrawing.html
                // If you use the AllPaintingInWmPaint attribute, system.drawing.dll will sometimes throw a System.ArgumentException claiming that you have used an Invalid Parameter. This happens if you follow the generally recommended procedure of disposing of the Graphics object at the end of the OnPaint handler
            }

            //g.Dispose();
        }

        Point windowPointToBitmapPoint(Point windowPoint)
        {
            if (_fitToScreenOrWindow)
            {
                //windowPoint.X = windowPoint.X * ActualTouchScreenSize.Width / this.ClientRectangle.Width;
                //windowPoint.Y = windowPoint.Y * ActualTouchScreenSize.Height / this.ClientRectangle.Height;

                // Since the touchscreen was resized proportionally, the top/bottom or left/right borders may be offset.
                float scale = Math.Min((float)(((float)this.ClientRectangle.Width) / ((float)_drawingBitmap.Width)), (float)(((float)this.ClientRectangle.Height) / ((float)_drawingBitmap.Height)));
                int scaledWidth = (int)(_drawingBitmap.Width * scale);
                int scaledHeight = (int)(_drawingBitmap.Height * scale);
                int dx = (this.ClientRectangle.Width - scaledWidth) / 2; // this is how many pixels the drawn image is form the left
                int dy = (this.ClientRectangle.Height - scaledHeight) / 2; // this is how many pixels the drawn image is form the top

                windowPoint.X = (int)Math.Round((windowPoint.X - dx) / scale); // Without the Math.Round, the int type coersion automatically rounds 399.99 to 399, but Math.Round(399.99) yields 400.
                windowPoint.Y = (int)Math.Round((windowPoint.Y - dy) / scale);
            }
            else
            {
                Point p2 = getDrawingOffset();
                windowPoint.Offset(-p2.X, -p2.Y);
            }

            return windowPoint;
        }

        Point getDrawingOffset()
        {
            int x = 0;
            int y = 0;
            if (!ActualTouchScreenSize.IsEmpty)
            {
                if (this.ClientRectangle.Width > ActualTouchScreenSize.Width)
                    x = (this.ClientRectangle.Width - ActualTouchScreenSize.Width) / 2;
                if (this.ClientRectangle.Height > ActualTouchScreenSize.Height)
                    y = (this.ClientRectangle.Height - ActualTouchScreenSize.Height) / 2;
            }

            return new Point(x, y);
        }

        private void TouchScreenInterface_Closed(object sender, EventArgs e)
        {
            if (_graphics != null)
            {
                _graphics.Dispose();
                _graphics = null;
            }
            if (_drawingBitmap != null)
            {
                _drawingBitmap.Dispose();
                _drawingBitmap = null;
            }
        }

        private void contextMenu1_Popup(object sender, EventArgs e)
        {
            tsmiRefreshDisplay.Enabled = _tcpConnection.Connected;
        }

        protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
        {
            // I do this at the form level to capture keypresses no matter where the cursor is.
            switch (keyData)
            {
                //case Keys.F1: // F1
                //    tsmiViewerAbout.PerformClick();
                //    break;

                //case Keys.F5: // F5
                //    tsmiRefreshDisplay.PerformClick();
                //    break;

                case Keys.Control | Keys.F5: // Ctrl+F5
                    _tcpConnection.RequestSnapshot(this.ClientRectangle);
                    break;

                case Keys.Control | Keys.X: // Ctrl+X
                    this.Close();
                    break;

                //case Keys.Control | Keys.M: // Ctrl+M
                //    tsmiViewerToggleMousePointer.PerformClick();
                //    break;

                case Keys.Alt | Keys.Enter: // Alt+Enter
                //case Keys.F11: // F11
                    tsmiViewerToggleFullscreen.PerformClick();
                    break;

                case Keys.Escape: // Escape
                case Keys.Space: // Space
                    //case Keys.Apps:  // Apps Key (aka Windows Menu key)
                    cmsViewerMenu.Show(this, 0, 0);
                    break;
            }

            return base.ProcessCmdKey(ref msg, keyData);
        }

        private void cmsViewerMenu_Opening(object sender, CancelEventArgs e)
        {
            // Only show the Refresh menu item when connected.
            tsmiRefreshDisplay.Visible = (_tcpConnection != null && _tcpConnection.Connected);
        }


        public void Reconnect(bool forceShowLoginForm, string loginFormMessage)
        {
            Reconnect(forceShowLoginForm, loginFormMessage, 100); // do it almost immediately, ie the short default delay
        }
        public void Reconnect(bool forceShowLoginForm, string loginFormMessage, int delayMilliseconds)
        {
            // Drop the connection if there is one.
            if (_tcpConnection != null)
            {
                try
                {
                    _tcpConnection.Dispose();
                }
                catch
                {
                }
                _tcpConnection = null;
            }

            // Start the login form and connect timer.
            _forceShowLoginForm = forceShowLoginForm;
            _loginFormMessage = loginFormMessage;
            startShowLoginFormTimerOnFormThread(delayMilliseconds);
        }

        delegate void startShowLoginFormTimerOnFormThread_Delegate(int delayMilliseconds);
        private void startShowLoginFormTimerOnFormThread(int delayMilliseconds)
        {
            // The timer MUST be started on the forms thread!  Otherwise it won't actually start.

            if (this.InvokeRequired)
            {
                startShowLoginFormTimerOnFormThread_Delegate func = new startShowLoginFormTimerOnFormThread_Delegate(startShowLoginFormTimerOnFormThread);
                this.Invoke(func, delayMilliseconds);
            }
            else
            {
                tmrShowSignInForm.Stop(); // ensure the timer is stopped before setting the delay... I probably don't need to do this but it shouldn't hurt.
                tmrShowSignInForm.Interval = delayMilliseconds;
                tmrShowSignInForm.Start(); // this shows the signin form on the forms thread, while allowing the tcp client to clean up.
            }
        }

        private void tmrShowSignInForm_Tick(object sender, EventArgs e)
        {
            tmrShowSignInForm.Stop();

            loginFormAndConnect(_forceShowLoginForm, _loginFormMessage);
        }

        private void tsmiFitToScreen_Click(object sender, EventArgs e)
        {
            ToggleFitToScreen();
        }


        protected override void WndProc(ref Message msg)
        {
            if (_kiosk)
                return;

            const int wmSysCommand = 0x0112;

            if (msg.Msg == wmSysCommand)
            {
                switch (msg.WParam.ToInt32())
                {
                    case 0xF030: // Maximize Window button pressed.   SC_MAXIMIZE=0xF030
                        // Intercept the maximize window message and set it to full screen.
                        fullscreen();
                        return; // return so that we don't pass the SC_MAXIMIZE message any further since we just went full screen.
                }
            }

            // Call base class function
            base.WndProc(ref msg);
        }

        internal void InvalidateFormOnFormThread()
        {
            try
            {
                if (this.InvokeRequired && this.IsDisposed == false)
                {
                    this.Invoke(new MethodInvoker(InvalidateFormOnFormThread));
                }
                else
                {
                    // Since Invalidate() does not immediately call OnPaint(), OnPaint() often times will get called
                    // while the app is already processing new draw commands and this can cause a flicker.
                    // So instead of calling Invalidate, call Refresh!!!
                    //this.Invalidate();
                    this.Refresh();
                }
            }
            catch // This can happen if the form is disposed while processing.
            {
                // do nothing
            }
        }

        private void tsmiSettings_Click(object sender, EventArgs e)
        {
            showMessageOnBlackBackground("");

            // Make the window bigger so we aren't stuck with a little window and a big signin window.
            if (_isFullscreen == false && (this.Size.Width < 1024 || this.Size.Height < 768))
                this.Size = new Size(1024, 768);

            createDrawingBitmap(this.ClientSize); // resize drawing bitmap so that when we draw connection status text it will fit and be centered.
            ActualTouchScreenSize = Size.Empty; // force OnPaint to draw at 0,0.

            Reconnect(true, ""); // show sign-in window.
        }

    }
}
